package xyz.lwm.lazycat.vertx;

import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.RoutingContext;
import xyz.lwm.lazycat.utility.CollectionUtil;
import xyz.lwm.lazycat.utility.CustomKeyMap;
import xyz.lwm.lazycat.utility.IoUtil;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.utility.exception.IoRuntimeException;
import xyz.lwm.lazycat.web.handle.ContentType;
import xyz.lwm.lazycat.web.handle.HandlerType;
import xyz.lwm.lazycat.web.handle.Header;
import xyz.lwm.lazycat.web.handle.HttpRequest;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpCookie;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * vertx http request
 *
 * @author lwm
 */
class VertxHttpRequest implements HttpRequest {

    private static final Pattern CHARSET_PATTERN = Pattern.compile("charset\\s*=\\s*([a-z0-9-]*)", Pattern.CASE_INSENSITIVE);

    private final RoutingContext rctx;

    private Map<String, List<String>> headersCache;

    private Map<String, HttpCookie> cookieCache;

    private Map<String, List<String>> paramsCache;

    private Map<String, String> pathParamsCache;

    private VertxMultipart multipartFormDataCache;

    private Charset charsetCache;

    private byte[] bodyCache;

    VertxHttpRequest(RoutingContext rctx) {
        this.rctx = rctx;
    }

    @Override
    public String getMethod() {
        return rctx.request().method().name();
    }

    @Override
    public URI getURI() {
        return URI.create(rctx.request().uri());
    }

    @Override
    public String getPath() {
        return rctx.request().path();
    }

    @Override
    public String getQuery() {
        return rctx.request().query();
    }

    @Override
    public Map<String, List<String>> getHeaders() {
        if (headersCache == null) {
            Map<String, List<String>> map = new HashMap<>();
            rctx.request().headers().forEach(e -> {
                map.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(e.getValue());
            });
            this.headersCache = map;
        }
        return headersCache;
    }

    @Override
    public String getHeader(String headerKey) {
        List<String> vals = getHeaders().get(headerKey);
        return CollectionUtil.isEmpty(vals) ? null : vals.get(0);
    }

    @Override
    public String getHeader(Header headerKey) {
        return getHeader(headerKey.toString());
    }

    @Override
    public String getHeader(String headerKey, Charset charset) {
        final String header = getHeader(headerKey);
        if (null != header) {
            byte[] bytes = header.getBytes(StandardCharsets.ISO_8859_1);
            return new String(bytes, charset);
        }
        return null;
    }

    @Override
    public String getContentType() {
        return getHeader(Header.CONTENT_TYPE);
    }

    @Override
    public Charset getCharset() {
        if (this.charsetCache == null) {
            It.of(getContentType()).filter(StringUtil::isNotBlank)
                    .let(CHARSET_PATTERN::matcher).filter(Matcher::find)
                    .let(matcher -> matcher.group(1))
                    .let(this::parseCharset)
                    .orElse(Charset.defaultCharset())
                    .also(charset -> this.charsetCache = charset);
        }
        return this.charsetCache;
    }

    private Charset parseCharset(String charset) {
        try {
            return Charset.forName(charset);
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public String getUserAgent() {
        return getHeader(Header.USER_AGENT);
    }

    @Override
    public String getCookiesString() {
        return getHeader(Header.COOKIE);
    }

    @Override
    public Collection<HttpCookie> getCookies() {
        return getCookieMap().values();
    }

    @Override
    public Map<String, HttpCookie> getCookieMap() {
        if (null == this.cookieCache) {
            String cookiesString = getCookiesString();
            if (StringUtil.isBlank(cookiesString)) {
                this.cookieCache = Collections.emptyMap();
            } else {
                this.cookieCache = HttpCookie.parse(cookiesString)
                        .stream().collect(Collectors.toMap(
                                HttpCookie::getName,
                                e -> e,
                                (v1, v2) -> v1,
                                () -> new CustomKeyMap<>(String::toLowerCase)));
            }
        }
        return cookieCache;
    }

    @Override
    public HttpCookie getCookie(String cookieName) {
        return getCookieMap().get(cookieName);
    }

    @Override
    public boolean isMultipart() {
        return rctx.request().isExpectMultipart();
    }

    @Override
    public boolean isForUrlencoded() {
        if (!HandlerType.POST.name().equalsIgnoreCase(getMethod())) {
            return false;
        }
        return It.of(getContentType()).filter(StringUtil::isNotBlank)
                .let(ContentType::isFormUrlEncode).getOr(false);
    }

    @Override
    public boolean isApplicationJson() {
        return It.of(getContentType()).filter(StringUtil::isNotBlank)
                .let(ContentType::isApplicationJson).getOr(false);
    }

    @Override
    public boolean isTextPlain() {
        return It.of(getContentType()).filter(StringUtil::isNotBlank)
                .let(ContentType::isTextPlain).getOr(false);
    }

    @Override
    public String getBody() {
        return getBody(getCharset());
    }

    @Override
    public String getBody(Charset charset) {
        return new String(getBodyBytes(), charset);
    }

    @Override
    public byte[] getBodyBytes() {
        if (this.bodyCache == null) {
            InputStream bodyStream = getBodyStream();
            if (bodyStream == null) {
                this.bodyCache = null;
            } else {
                this.bodyCache = IoUtil.readBytes(bodyStream, true);
            }
        }
        return this.bodyCache;
    }

    @Override
    public InputStream getBodyStream() {
        Buffer buffer = rctx.body().buffer();
        return buffer == null ? null : new ByteArrayInputStream(buffer.getBytes());
    }

    @Override
    public String getParam(String name) {
        return It.of(getParams(name))
                .let(list -> list.get(0))
                .getOr(null);
    }

    @Override
    public List<String> getParams(String name) {
        return getParams().get(name);
    }

    @Override
    public Map<String, List<String>> getParams() {
        if (this.paramsCache == null) {
            this.paramsCache = new HashMap<>();
            rctx.request().params();
            It.of(rctx.request().params()).filter(Objects::nonNull)
                    .also(params -> params.forEach(e ->
                        this.paramsCache.computeIfAbsent(e.getKey(),
                                k -> new ArrayList<>()).add(e.getValue())));
        }
        return this.paramsCache;
    }

    @Override
    public String getPathParam(String name) {
        return getPathParams().get(name);
    }

    @Override
    public Map<String, String> getPathParams() {
        if (pathParamsCache == null) {
            pathParamsCache = new LinkedHashMap<>();
        }
        return pathParamsCache;
    }

    @Override
    public VertxMultipart getMultipart() {
        if (this.multipartFormDataCache == null) {
            this.multipartFormDataCache = parseMultipart();
        }
        return this.multipartFormDataCache;
    }

    private VertxMultipart parseMultipart() throws IoRuntimeException {
        return new VertxMultipart(rctx);
    }

}
